feat(mcp): an agent can finally read back what it recorded to the SD card - #511
Conversation
An agent could start an SD recording through MCP but had no way to see what was on the card or get any of it back. Adds list_sd_files, get_sd_storage, download_sd_file (raw file + parsed CSV) and delete_sd_file over Core's existing SD surface, closing the loop start -> stop -> list -> download -> CSV. Also fixes SdCardDeviceConfiguration.FromDevice, which folded the live Channels view and could throw "Collection was modified" when the consumer thread repopulated channels underneath it. closes #500 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by Qodofeat(mcp): add SD-card retrieval tools with optional CSV export
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo
1.
|
…SD file names Qodo round 1. A CSV export that throws after the file was created left it on disk with CsvPath=null, so nobody could name it to clean it up; it is now deleted on every failure path including cancellation. And the listing pre-flight ran before Core's SCPI-safety check, so a name containing newlines produced a multi-line 'no such file' message instead of a plain rejection — the same character rule now applies at the tool boundary. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 39ebfca |
Qodo round 2. CsvExporter writes every row through WriteAsync, but the output FileStream was opened without useAsync, so those went through the thread pool rather than the OS async path — while the input stream it reads from already used it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit ceb98eb |
Qodo round 3. The comment asserted a relationship nothing enforces, so tuning one side would have made it a lie. The two buffers stay separate — read-a-log-at-a-time is not write-a-CSV-at-a-time — and the doc now says that instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit c4872f7 |
|
Qodo-clean, CI green — ready for review. Round 4 on head Rounds 1-3 took: a half-written CSV left on disk when an export threw (now deleted on every failure path, cancellation included); a file name with control characters reaching my listing pre-flight ahead of Core’\s SCPI-safety check and producing a multi-line "no such file" message (the same character rule now applies at the tool boundary, with tests on both sides of it); and the exported CSV being opened for synchronous writes while the log it reads was already async. Round 3 was a comment of mine claiming the CSV write buffer tracked the parse buffer — it does not, and the two are deliberately independent, so the doc changed rather than the code. Full suite re-run green after every push: net9.0 (2964 Core + 95 Mcp) and net10.0 (2964), 0 failures, 0 warnings. Not merging — this is for your review. |
What was wrong
An agent driving a DAQiFi through the MCP server could start an SD recording it could never read. The server had
start_sd_loggingandstop_sd_loggingand nothing else: no way to see what was on the card, no way to check whether there was room to record, no way to fetch a log back, and no way to turn one into something it could actually analyse. The card was a write-only device. Ask "log for 10 seconds and tell me the average on AI0" and the agent could do the first half and then had nothing.Core has shipped the whole retrieval surface for a long time — listing, storage, download, delete, and format-detecting parsers that feed the CSV exporter. None of it was reachable from a tool call.
How it was fixed
Four tools over the Core APIs that were already there:
list_sd_files,get_sd_storage,download_sd_fileanddelete_sd_file.download_sd_filefetches the raw file and, by default, parses it and writes a CSV beside it, returning both paths — so one call closes the loop from "there is a file on the card" to "here is a table I can read".What a reviewer may want to push back on:
download_sd_fileis not blocked by--read-only; onlydelete_sd_fileis. Reading data off the card changes nothing on the device, and a read-only server that cannot read the data would be a strange thing. It does write two files into this machine's temp directory, which is the only way the bytes can reach the agent at all. Stated in the tool description and the README rather than left implicit.csvErrorsays what happened. The one case that does fail is a log that parses to zero samples, because a header-only CSV is indistinguishable from a successful export of an empty file — that one deletes the stub and says so.SdCardDeviceConfiguration.FromDevicefolded the liveChannelsview, whichDaqifiDevicedocuments as unsafe off the consumer thread — a status message repopulating the collection mid-enumeration throws "Collection was modified", and it read the collection twice so the analog and digital counts could disagree. It now takes one lock-protected snapshot. This is on the download path (the live device is what supplies the timestamp clock that firmware ≤3.7.2 omits from SD logs), so the MCP server would hit it; the example app calls it in the same place.<download>.csv, not by swapping the extension. The firmware logs in CSV as well as protobuf, and Core's temp file keeps the device-side extension — soChangeExtension(".csv")names the very file being read and truncates the download on the way to parsing it. A test pins it.Verification
Tests — 40 new (33 in
Daqifi.Mcp.Tests, 7 inDaqifi.Core.Tests). Two are proven regression catchers: reverting the Core snapshot fix failsFromDevice_WhileStatusMessagesRepopulateChannels_DoesNotThrow2/2 runs, and restoringChangeExtensionfailsCsvSourceFile_IsNotOverwrittenByItsOwnExport. The parse-and-export chain is covered end to end against a synthetic on-disk log, and the reported CSV row count is checked against the lines Core's real exporter writes rather than against the rule the counter was written from. Full suite green on net9.0 (2964 Core + 88 Mcp) and net10.0 (2964), 0 failures, 0 warnings.Bench (non-destructive), Nq1 fw 3.7.2 on
/dev/cu.usbmodem1101— driven as a real MCP server over stdio JSON-RPC, not through the agent class:configure_analog_channels [0]→set_sample_rate 50→start_sd_logging→ 1.2 s →stop_sd_logging→list_sd_files(46→47 files, the newlog_20260812_165615.binlisted at 430 B) →download_sd_file→ 430 B fetched in 0.68 s, 43 samples, 43 CSV rows, reported count equal to the CSV's actual data lines, timestamps 20 ms apart as commanded (which is also the check that the live device's 42 MHz clock reached the parser — the 50 MHz fallback would have stretched them ~19%).get_sd_storage7.80 GB total / 100% free;list_sd_files45–47 entries with sizes and dates; the same file downloaded again under a fully upper-cased name and resolved;does_not_exist.binrefused in 2.1 s naming what is on the card, where before the fix it stalled for 20 s.--read-onlyrefusals are unit-tested only. The bench run of that server did not complete: after the stalled transfers the device stopped answering SCPI (discovery intermittent, then silent; connect times out at the channel-configuration wait). Confirmed device-side, not a regression — the example CLI fails identically on the same port, and the same MCP binary had connected six times earlier in the session. It is the documented transient state whose recovery is a replug. Nothing here writes to the device on that path anyway: the read-only refusal throws before any I/O.closes #500
Not merging — this is for your review.